package org.orbroker

import org.orbroker.exception._
import org.orbroker.util.Mirror._
import org.orbroker.adapt.{ BrokerAdapter, ProcedureMetadata }
import scala.collection.immutable.Map
import org.orbroker.callback.ExecutionCallback
import org.orbroker.conv.ParmConverter
import java.sql.{ SQLException, CallableStatement, ParameterMetaData, Connection }
import java.sql.ParameterMetaData._

private[orbroker] trait CallStatement extends StaticStatement with ResultSetProducer with GenKeyProducer {

  def call[OP, T](
    token: Token[T],
    session: Session,
    parms: Map[String, _],
    keyHandler: Option[(T) ⇒ Unit],
    receivers: Seq[(T) ⇒ Boolean],
    outParmHandler: Option[(OutParms) ⇒ OP]) = {
    val parsed = statement(parms)
    val cs = parsed.prepareCall(session.connection, receivers.size > 0)
    try {
      setFeatures(cs, session)
      val values = setParms(token, cs, parsed.parmDefs, parms, parsed.callInfo)
      val hasResultSet = cs.execute()
      val rowsUpdated = cs.getUpdateCount
      if (hasResultSet) {
        val elements: Iterator[(T) ⇒ Boolean] = receivers.iterator
        var rs = cs.getResultSet
        while (rs != null && elements.hasNext) {
          val next: (T) ⇒ Boolean = elements.next()
          mapResult(token.extractor, rs, next)
          rs = if (cs.getMoreResults) cs.getResultSet else null
        }
      }
      if (rowsUpdated > 0) for (kh ← keyHandler) {
        handleGeneratedKeys(token, kh, cs.getGeneratedKeys, rowsUpdated)
      }

      val outParm: OP = outParmHandler match {
        case Some(handler) ⇒ {
          val outParms = new OutParmsImpl(token.id, parsed.parmIdxMap, cs, callback, adapter)
          handler(outParms)
        }
        case None ⇒ null.asInstanceOf[OP]
      }
      (parsed.sql, values, rowsUpdated, outParm)
    } finally {
      import JdbcCloser._
      checkWarningsAndClose(cs, callback)
    }
  }

  private def setParms(token: Token[_], cs: CallableStatement, parmDefs: Seq[String], parms: Map[String, _], callInfo: ProcedureMetadata) = {
    val values = new scala.Array[Any](parmDefs.size)
    var parmIdx = 0
    for (parmDef ← parmDefs) try {
      parmIdx += 1
      if (callInfo.isOutParm(parmIdx)) registerOutParm(callInfo, cs, parmIdx, parmDef)
      if (callInfo.isInParm(parmIdx)) values(parmIdx - 1) = setParm(token, cs, parmIdx, parmDef, parms)
    } catch {
      case be: BrokerException ⇒ throw be
      case th ⇒ throw new ConfigurationException("Statement '%s', could not set parameter: %s".format(id.name, parmDef), th)
    }
    values
  }

  private def registerOutParm(
    callInfo: ProcedureMetadata, cs: CallableStatement,
    parmIdx: Int, parmDef: String) =
    try {
      val pType = callInfo.parmType(parmIdx)
      cs.registerOutParameter(parmIdx, pType)
    } catch {
      case e: SQLException ⇒
        throw new ConfigurationException("Statement " + id + ", failed to register OUT/INOUT parameter :" + parmDef, e)
    }

}